Android 13 Developer Preview一览
不要吃惊。Android 12才刚刚正式推出没几个月时间呢,Android 13就已经来了。
是的,Android 13 Developer Preview目前已经推出了两个版本迭代了,包含了众多的新特性和行为变更。
本来按照往年的习惯,我是不会在如此早期的阶段就去研究每年新系统的新功能的。但是由于最近公司安排我去做一次内部的技术分享涉及到了这个主题,所以今年第一时间就对Android 13进行了还算比较全面的体验。
由于我准备这次分享时所占用的都是我个人的休息时间,所以我并不认为这些内容会受到公司的知识产权保护。因此今天就写一篇对外的文章,将我对Android 13 Developer Preview的学习与理解分享出来。
首先解释一下为什么Android 13这么快就到来了。
上图是Google每年发布Android新系统的时间表。可以看到,在每年的年初,Google就会发布新版Android系统的Developer Preview版本。到了每年的中上旬,会发布Beta Release版本。而到了中下旬的时候,会进入平台稳定期,这时会发布Release Candidate版本。到了年底的时候,才会正式推出新版Android系统的Stable版本。
所以,Android 12 Stable版本在去年年底刚刚推出,紧接着Android 13的Developer Preview版本马上就来了。
如果你想要现在就对Android 13进行尝鲜,那么主要有两种办法,一是使用一台Pixel 4或更高版本的Pixel系统手机,二是使用Android Studio自带的模拟器。
使用手机的话需要进行刷机才可以,使用模拟器就很简单了,下载最新版的系统镜像即可。具体操作步骤我就不在这里演示了,详情请参考官方文档:
https://developer.android.google.cn/about/versions/13/get
完成以上步骤后,你就可以得到最新的Android 13系统了,如下所示:
我们也可以到设置里面去检查一下当前系统的版本号,如果你看到显示的是13或者Tiramisu(Android 13的内部代号),那么就代表你已经成功了。
接下来我们去研究一下Android 13具体带来了哪些新功能和变化,下图是我从官网截取的一张Developer Preview 1的新功能与变化:
其实在我准备这篇文章的时候,Google正好推出了Developer Preview 2的版本,所以如果你现在再去官网查看Android 13的新功能与变化,会发现比我这里列出的会多出很多。
不过我也没有指望通过一篇文章就能把所有的新功能全部覆盖全了,因此这里我们就还是先只专注于Developer Preview 1的版本吧。
从上图可以看出,Android 13的新功能与变化主要可以分为4个部分,核心功能、图像、隐私与安全、用户体验。
其中核心功能部分主要就是增加了一些对Java 11的API支持,由于现在Android程序基本都是在使用Kotlin进行开发了,所以这个功能对于我们来说影响不大。
而图像这部分并不是我的技术专长,我并没有信心能将这部分内容讲清楚,因此这里也就跳过了。
那么接下来的隐私与安全,以及用户体验是我们的重点,我会将图中列举出来的每一条新功能与变更,都展开进行讲解。
/ 新增WIFI运行时权限 /
去年,Google在Android 12当中新增了几个蓝牙相关的运行时权限。原因是因为当开发者去访问一些蓝牙相关的接口时,却需要申请地理位置权限才行。
这是一个历史遗留问题,为了更好地保护用户隐私,Google在Android 12当中增加了BLUETOOTH_SCAN,BLUETOOTH_ADVERTISE,BLUETOOTH_CONNECT,这3个运行时权限。这样当开发者需要访问蓝牙相关的接口时,只需要请求这些蓝牙权限即可。详情可以参考这篇文章 PermissionX 1.6发布,支持Android 12 。
而在今年的Android 13当中,Google将保护用户隐私延伸到了WIFI领域。
和蓝牙类似,当开发者去访问一些WIFI相关的接口时,如热点、WIFI直连、WIFI RTT等,也需要申请地理位置权限才行。
这其实也是一个历史遗留问题,用户肯定无法理解为什么使用一些WIFI功能时却需要授权地理位置权限。
为此,Android 13当中新增了一个NEARBY_WIFI_DEVICES权限,当再使用以上场景相关的WIFI API时,我们只需申请NEARBY_WIFI_DEVICES权限即可,从而更好地保护了用户的隐私。
由于NEARBY_WIFI_DEVICES只是一个普通的运行时权限,并且它和别的运行时权限用法并无任何区别,因此这里我就不通过代码来演示它的用法了。
如果想要查看关于这个权限更加详细的资料,可以参考官方网站:
https://developer.android.google.cn/about/versions/13/features/nearby-wifi-devices-permission
/ Intent filter屏蔽不匹配的Intent /
在Android 13上,这是一个很重要的安全变更,如果不充分了解它的话,到时候可能会出现一系列的崩溃问题。
我们先来看一下关于这个安全变更的官方定义。当你的App通过Intent和其他App交互时,如果该App的targetSdkVersion指定成了Android 13或者更高,那么这个Intent必须匹配了Intent filter中定义的某项元素,才能被成功传递出去。
官方的定义看上去有点拗口,下面我通过一个简单的例子来将它讲述清楚。
比如说我们在App A的AndroidManifest.xml文件中定义了如下内容:
<activity
android:name="com.example.android13test.SharedActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
可以看到,这里将SharedActivity的exported属性设置成了true,说明它是可以被外部其他App调用的。然后我们在<intent-filter>标签中声明了它的action是send,category是default。
那么,在App B当中,自然而然就可以使用如下代码来启动App A当中的SharedActivity:
val intent = Intent(Intent.ACTION_SEND)
startActivity(intent)
这里我们在Intent当中指定了ACTION_SEND,匹配了SharedActivity中定义的action。而startActivity()方法又会自动给Intent添加一个default的category,所以它又匹配了SharedActivity中定义的category。action和category同时都匹配上了,那么自然是可以启动成功的。
但除了使用上述代码之外,我们通过以下写法同样也可以成功启动SharedActivity:
val intent = Intent()
intent.component = ComponentName(
"com.example.android13test",
"com.example.android13test.SharedActivity")
startActivity(intent)
这里通过ComponentName明确指定了App A的包名以及SharedActivity的完整类名,即使没有指定action和category,依然是可以正常工作的,对不对?
没错,但仅限于Android 12及以下系统。
Android 13上的这项新的安全变更,就是限制这种没有匹配Intent filter中定义的任何元素,但是却依然可以跨程序进行交互的安全漏洞。因为你的Activity可能会以某种非你所设计的方式被其他外部程序调用。
不过这项安全变更限制有以下几种例外情况:
目标Activity并没有定义任何的Intent filter,这种情况下仍然可以通过指定包名和类名来启动目标Activity。
自己应用内的组件之间进行交互不会受此限制。
系统发出的Intent不会受此限制。
拥有Root用户权限发出的Intent不会受此限制。
看到这里,希望大家都能对自己的App在跨程序Intent使用方面进行一下审查,如果发现有违规使用的地方尽早修复,避免到时候在Android 13上出现大面积崩溃。
/ 图片选择器 /
关于图片选择器,我只能说,这是一个救星级的新功能。
Android苦图片选择器久矣,系统自带的相册选择器实在是太烂了,我都没有见到过几个应用会使用它,基本都是各个App自己去实现图片选择器。
自己实现图片选择器则会带来几个问题。第一,实现起来比较复杂,要写很多的代码,成本很高。第二,需要申请Storage权限来访问用户的本地存储空间,对用户隐私不够友好。
所以,Android 13当中,Google终于把图片选择器提上了日程,打造一个功能强大,高度可用的系统自带图片选择器,使各个App不用再自己造轮子了。同时,由于这是系统提供的图片选择器,App也不需要再请求Storage权限了,对用户隐私更加友好。
根据官方的描述,这个图片选择器会非常强大,不仅性能高效,还深度可定制,甚至能对图片进行搜索、排序等等。不过我暂时没能体会到这么强大的功能,不知道是不是还没有完善。但有一点我是确定的,它的用法真的非常简单。
具体有多简单呢?说出来怕你不信,两行代码就搞定了:
val intent = Intent(MediaStore.ACTION_PICK_IMAGES)
startActivityForResult(intent, 1)
是的,这样就可以把系统自带的图片选择器打开了。后面的一切功能都与我们无关,用户会在系统提供的图片选择器界面当中浏览和选择图片,最后将选中图片的Uri通过onActivityResult()回调返回给我们:
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == 1 && resultCode == Activity.RESULT_OK) {
val uri = data?.data
Glide.with(this).load(uri).into(findViewById(R.id.image_view))
}
}
这里我将获取到的图片Uri传递给Glide,从而让图片显示出来。效果如下图所示:
整个选择图片流程的体验极佳。要知道,我们只付出了极小的代价(基本等于零),甚至连Storage权限都没有申请。
但是细心的朋友可能注意到了,这个图片选择器一开始是一个半屏的状态,需要我们手动拖拽才能让它变成全屏。
这个是Google故意设计成这样的,如果我们一次性只能选择一张图片的话,默认就是半屏的状态。
而如果我们一次性允许选择多张图的话(比如微信一次最多可以选择9张图片),那么它默认就会变成全屏的状态。
下面我们就通过代码来验证一下吧,指定允许一次性选择多张图片非常简单,如下所示:
val maxNum = 5
val intent = Intent(MediaStore.ACTION_PICK_IMAGES)
intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, maxNum)
startActivityForResult(intent, 1)
可以看到,这里通过传入一个EXTRA_PICK_IMAGES_MAX参数,就能指定一次性最多允许选择多少张图片了。效果如下图所示:
接下来还有一个问题,现在我们一次性选择了多张图片,那么怎样才能把用户选中的这些图片的Uri全部获取到呢?
这种情况下获取Uri的API与之前有所不同,但也非常简单,写法如下:
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == 1 && resultCode == Activity.RESULT_OK) {
data?.let {
it.clipData?.let { clipData ->
for (i in 0 until clipData.itemCount) {
val uri = clipData.getItemAt(i).uri
// Handle uri with your logic
}
}
}
}
}
我个人觉得,系统自带的图片选择器应该是Android 13中我最喜欢的一个功能了。但是它也有一个很明显的缺点,就是只能应用在Android 13系统上,Android 12及以下的系统,我们仍然需要编写自己的图片选择器才行,这个问题短期之内还是无法解决的。
到这里我们就把Android 13隐私与安全这部分的功能与变更都介绍完了,接下来进入用户体验部分的内容。
/ 主题应用图标 /
主题应用图标这个功能也算是被许多国内手机厂商做烂的一个功能了,如今Google终于把它给纳入标准化了。
具体来讲,就是桌面上每个应用程序的图标风格迥异,各不相同,而有些用户可能更加偏爱使用全局统一风格的图标。
主题应用图标就是为了解决这个问题的,它可以使桌面上的所有应用图标都使用同一种主题风格,效果如下图所示:
看上去系统帮我们把所有事情都做好了,那我们还需要做什么呢?
其实事情并没有那么简单,假如你把自己开发的App图标也拖到桌面,就会发现出问题了:
可以看到,我们自己开发的App的图标并没有像其他App那样变成统一的主题风格。同时也就意味着,这个功能是需要适配的。
好在适配的方案并不复杂,首先我们需要准备一张符合规格的图标。具体是哪些规格呢?
1. 必须是一张VectorDrawable图片。
2. 图标应该是在一个90 * 90 dp的容器当中,logo部分应该只有36 * 36 dp的尺寸大小,最大不能超过60 * 60 dp,否则存在被切掉的风险。
3. 图标应该是扁平的,如果你的图标必须是3d的,可以用透明度来表示立体效果。
Google给出的官方设计示意图如下:
准备好了图标,接下来的事情就很简单了。
还记得在Android 8.0系统中Google引入的应用图标前景背景层分离的功能吗?(具体可以参考这篇文章,一起来学习Android 8.0系统的应用图标适配吧),现在我们只需要在其基础之上增加一个新的标签<monochrome>即可。
修改res/mipmap-anydpi-v26/ic_launcher.xml文件,如下所示:
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
可以看到,我们通过<monochrome>标签又指定了一个图标,而这里指定的就是我们在上个步骤当中设计的符合规格的图标。
当然我并没有再去单独设计一个图标,而是直接复用了创建项目时自动生成的前景图标。现在重新运行一下程序,效果如下图所示:
主题应用图标的适配就此完成。
/ Quick Settings API /
要解释什么是Quick Settings API,首先我们得知道什么是Quick Settings。
你可能会不知道Quick Settings是什么,但是我保证,你一定用过它。
当把手机的通知栏往下滑,上方的那些快捷开关就是Quick Settings。像我们平时去打开Wifi,打开手电筒,都会用到Quick Settings功能。
可是你是否知道,除了使用系统自带的这些Quick Settings之外,我们也可以添加自己自定义的Quick Settings。
要创建一个自定义的Quick Settings非常简单,只需要先创建一个自定义的Serivce并让它继承自TileService,如下所示:
public class MyQSTileService extends TileService {
}
然后把这个MyQSTileService按如下规则在AndroidManifest.xml中进行注册即可:
<service
android:name="com.example.android13test.MyQSTileService"
android:label="My Test Tile"
android:icon="@drawable/ic_launcher_foreground"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
android:exported="true">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
</service>
现在重新运行一下程序,我们自定义的Quick Settings就已经注册到系统中了,但是你会发现你是无法找到它的:
这是因为自定义的Quick Settings默认都不会处于激动状态的,需要我们手动激活它才可以使用:
可以看到,我们需要点击编辑按钮,然后在候选区域找到我们自定义的Quick Settings,再通过拖拽的方式将它拖到激活区域,这样才可以使用它。
有没有感到操作非常的不便?对于许多的小白用户来说,或许他们永远都发现不了这个隐蔽的Quick Settings。
而Quick Settings API就是为了解决这个问题的。
有了Quick Settings API,我们可以以弹窗的形式告知用户是否要添加我们自定义的Quick Settings。只要用户点击了同意,就可以一键完成添加,而不再需要像之前那样烦琐的操作了。
那么具体要如何实现呢?我就直接演示代码了,如下所示:
val manager = getSystemService(Context.STATUS_BAR_SERVICE) as StatusBarManager
val componentName = ComponentName(
"com.example.android13test",
"com.example.android13test.MyQSTileService")
val icon = Icon.createWithResource(this, R.drawable.ic_launcher_foreground)
manager.requestAddTileService(componentName, "My Title Test", icon, {}, {})
可以看到,StatusBarManager类当中新增了一个requestAddTileService()函数,就是用于添加自定义Quick Settings的。
这个函数接收的参数有点多,但是我们可以只传最关键的参数,比如组件的类名,图标,Title等。最后两个参数不是强制的,可以用于监听添加是否成功,我就直接传了空参数。
那么运行这段代码,效果如下图所示:
我们事先检查了一下,已激活的Quick Settings当中是没有我们自定义的Quick Settings的。
接下来点击按钮即可执行上面的代码片段,这时你会看到系统弹出了一个Dialog来询问用户是否要添加自定义的Quick Settings,注意这个Dialog是系统提供的,因此我们不能修改它的UI。
当用户点击了同意,即可添加成功,这个时候再去已激活的Quick Settings当中检查一下,就能发现我们自定义的Quick Settings了。
/ 应用单独语言设置 /
长久以来,Android手机都只能设置一个系统全局的语言。设定了这个语言之后,手机上安装的所有App只要支持这种语言的,都会显示这个语言的文字,如果不支持的话,就会显示默认语言文字。
这种全局只能设置一个语言的规则,在某些情况下是非常不便的。比如说我的手机语言设定的是中文,但是有些特定的App我需要它显示英文。这种情况下我就只能切换系统语言去使用那个App,用完之后还得再切回来。而每次切换系统语言都是非常耗资源的,我的手机也会因此卡上一会。
Android 13终于在这方面带来了好消息,它允许我们为每一个App单独设置语言,从而可以无视系统全局的语言设定了。这个功能可以说是拯救了像我这样的用户。
那么具体要如何操作呢?我通过下图给大家演示一下:
我们进入系统设置,选择System -> Language & Input,你会发现多出了一个App Languages的选项。点击进入之后,手机上安装的所有应用程序都会列出来,然后就可以为每一个App单独设置语言了。
这个功能不需要App去做任何的适配,只要是Android 13及以上系统自动就会有。
但是如果你的App想做一些额外的事情,其实也是可以的。
Google现在允许我们在App的内部通过调用API直接切换App的语言,这样就不需要用户手动去系统设置中操作了。
这个API是Android 13系统新增的,但是考虑到向下兼容性的问题,Google在AndroidX库中提供了一个向下兼容的API,使得我们在Android 13以下的系统中也可以切换App的语言,所以我就直接使用AndroidX提供的API了:
val appLocale: LocaleListCompat = LocaleListCompat.forLanguageTags("xx-YY")
AppCompatDelegate.setApplicationLocales(appLocale)
LocaleListCompat.forLanguageTags()函数中的xx-YY代表的是语言和地区,比如en-US、zh-CN。
调用上述代码之后,你的App会经历一次重启,然后就会使用设置的新语言了。
/ 快速断字连接符 /
断字连接符这个功能我个人感觉比较鸡肋,因为它的单词分段总是达不到我期望中的效果,所以也很少会去使用它。但是Android 13的新功能里有关于断字连接符的部分,所以还是讲一下吧。
所谓的断字连接符,指的就是有些英文句子中的某些单词比较长,放在同一行里显示不下,但是放到下一行显示,上一行的内容就显得很空荡松散了,所以这个时候就可以选择使用断字连接符。
使用断字连接符前:
使用断字连接符后:
要想启用断字连接符功能其实非常简单,只需要给你的TextView指定以下属性就可以了:
<TextView
android:hyphenationFrequency="[full|normal]"
/>
指定的参数会决定TextView以何种频率去计算哪里应该使用断字连接符,normal的话频率就会低一些,full的话频率就会高一些。
不过这其实并不是什么新功能,早在Android 6.0的时候就已经有这个功能了,但是从Android 10开始,这个功能被默认关闭了。
Google关闭它的原因主要还是性能,因为判断哪里需要使用断字连接符得进行大量的计算才行,这就会拖慢TextView的渲染速度。
而Android 13中引入的这个快速断字连接符,就是为了解决性能问题的。
根据Google的官方文档描述,快速断字连接符的性能相比之前提升了200%,对于TextView的渲染速度已经几乎没有影响了,所以我们可以放心地使用它。
使用方式其实就是在之前的属性后面加个Fast即可,如fullFast或normalFast:
<TextView
android:hyphenationFrequency="[fullFast|normalFast]"
/>
但是正如我前面所说的,Android上的断字连接符,它的分段效果我认为是不够好的。所以即使性能提升了200%,对我来说帮助也不是很大吧。
/ 结语 /
以上就是关于Android 13 Developer Preview 1的所有内容了。
当然现在Developer Preview 2也已经出来了,并且包含了更多的新功能与变化。但是很明显,本篇文章已经涵盖不了那么多内容了,所以就剩下的就等待你自己去探索了。